跳到主要内容

Java Servlet 学习之环境配置

Servlet 是什么?

Java Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自 Web 浏览器或其他 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。

Java Servlet 是运行在带有支持 Java Servlet 规范的解释器 的 web 服务器(Tomcat)上的 Java 类。

注意:安装 Tomcat 这部分就是看那篇笔记,这里就不再重复赘述了~

Servlet 可以使用 javax.servletjavax.servlet.http 包创建,它是 Java 企业版的标准组成部分,Java 企业版是支持大型开发项目的 Java 类库的扩展版本。

这些类实现 Java Servlet 和 JSP 规范(Java Server Pages)。在写本教程的时候,二者相应的版本分别是 Java Servlet 2.5 和 JSP 2.1。

Java Servlet 就像任何其他的 Java 类一样已经被创建和编译。在您安装 Servlet 包并把它们添加到您的计算机上的 Classpath 类路径中之后,您就可以通过 JDK 的 Java 编译器或任何其他编译器来编译 Servlet。

Servlet、Server 和 Service 的区别

Servlet(Server Applet)称为小服务程序或服务连接器,用 Java 编写的服务器端程序,具有独立于平台和协议的特性(说白了就是使用 Java提供的 Web API 编写的小程序)

当针对一个 JSP的第一个请求到来时,该页面转化为对应于 JSP中的指令的 JAVA类。容器负责创建对象,实际上就是一个 Servlet。

Server 表示 web 服务器,常规意义的服务器就是指它

Service 就是使用 HTTP、XML 或 JSON 相互通信的一种方式,不需要任何人参与。(就是表示用于交换数据的一个过程,并不是实体)

Servlet 架构

下图显示了 Servlet 在 Web 应用程序中的位置。

在JavaEE平台上,处理 TCP 连接,解析 HTTP 协议这些底层工作统统扔给现成的 Web服务器去做,我们只需要把自己的应用程序跑在 Web服务器上。为了实现这一目的,JavaEE提供了 Servlet API,我们使用 Servlet API 编写自己的 Servlet 来处理 HTTP 请求,Web服务器实现 Servlet API 接口,实现底层功能:

                 ┌───────────┐
│My Servlet │
├───────────┤
│Servlet API│
┌───────┐ HTTP ├───────────┤
│Browser│<──────>│Web Server │
└───────┘ └───────────┘

编写一个最简单的 Servlet

项目结构

整个工程结构如下:

web-servlet-hello
├── pom.xml
└── src
└── main
├── java
│ └── com
│ └── itranswarp
│ └── learnjava
│ └── servlet
│ └── HelloServlet.java
├── resources
└── webapp
└── WEB-INF
└── web.xml

添加 API 包

Servlet API 是一个 jar 包,我们需要通过 Maven 来引入它,才能正常编译。

编写 pom.xml 文件如下:

<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.itranswarp.learnjava</groupId>
<artifactId>web-servlet-hello</artifactId>
<packaging>war</packaging>
<version>1.0-SNAPSHOT</version>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<java.version>11</java.version>
</properties>

<dependencies>
<!-- 注意,得 4.0 版本才能直接使用注解 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.0</version>
<scope>provided</scope>
</dependency>
</dependencies>

<!-- 这里可以设置打包的名字,默认就是使用这个做根路径的 -->
<build>
<finalName>studyjavaservlet</finalName>
</build>
</project>

注意到这个 pom.xml 与普通 Java程序有个区别,打包类型不是 jar,而是 war,表示 Java Web Application Archive:

<packaging>war</packaging>

引入的 Servlet API 如下:

注意到 <scope> 指定为 provided,表示编译时使用,但不会打包到 .war 文件中,因为运行期 Web 服务器本身已经提供了 Servlet API 相关的 jar 包。

我们还需要在工程目录下创建一个 web.xml 描述文件,放到 src/main/webapp/WEB-INF 目录下(固定目录结构,不要修改路径,注意大小写)。

文件内容可以固定如下:

<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<display-name>Archetype Created Web Application</display-name>
</web-app>

编写 Servlet

一个 Servlet 总是继承自 HttpServlet,然后覆写 doGet()doPost() 方法。注意到 doGet() 方法传入了 HttpServletRequest 和 HttpServletResponse 两个对象,分别代表 HTTP请求和响应。

我们使用 Servlet API 时,并不直接与底层 TCP 交互,也不需要解析 HTTP 协议,因为 HttpServletRequest 和 HttpServletResponse 就已经封装好了请求和响应。

以发送响应为例,我们只需要设置正确的响应类型,然后获取 PrintWriter,写入响应即可。

如下代码所示:

// WebServlet注解表示这是一个Servlet,并映射到地址/:
@WebServlet(urlPatterns = "/")
public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 设置响应类型:
resp.setContentType("text/html");
// 获取输出流:
PrintWriter pw = resp.getWriter();
// 写入响应:
pw.write("<h1>Hello, world!</h1>");
// 最后不要忘记flush强制输出:
pw.flush();
}
}

@WebServlet 注解的使用

使用 @WebServlet 注解,可以简化繁琐的 xml 配置。使用注解前的 web.xml 配置如下:

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">

<servlet>
<servlet-name>helloServlet</servlet-name>
<servlet-class>coderead.servlet.HelloServlet</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>helloServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>

</web-app>

改为使用注解以后的 web.xml 配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">

</web-app>

编译后在容器中运行

运行 Maven 命令 mvn clean package,在 target 目录下得到一个 hello.war 文件,这个文件就是我们编译打包后的 Web应用程序。

现在问题又来了:我们应该如何运行这个 war 文件?

普通的 Java程序是通过启动 JVM,然后执行 main() 方法开始运行。但是 Web应用程序有所不同,我们无法直接运行 war 文件,必须先启动 Web服务器,再由 Web服务器加载我们编写的 HelloServlet,这样就可以让 HelloServlet 处理浏览器发送的请求。

因此,我们首先要找一个支持 Servlet API 的 Web 服务器。常用的服务器有:

  • Tomcat:由Apache开发的开源免费服务器;
  • Jetty:由Eclipse开发的开源免费服务器;
  • GlassFish:一个开源的全功能JavaEE服务器。

无论使用哪个服务器,只要它支持 Servlet API 4.0(因为我们引入的 Servlet 版本是4.0),我们的 war 包都可以在上面运行。

这里我们选择使用最广泛的开源免费的 Tomcat 服务器。

hello.war 复制到 Tomcat 的 webapps 目录下,然后切换到 bin 目录,执行 startup.shstartup.bat 启动 Tomcat 服务器:

访问测试(这里使用子域名)

为啥路径是 /studyjavaservlet/ 而不是 /? 因为一个 Web服务器允许同时运行多个 Web App,而我们的 Web App 叫 studyjavaservlet,因此,第一级目录 /studyjavaservlet/ 表示 Web App 的名字,后面的 / 才是我们在 HelloServlet 中映射的路径。(上面那个 ROOT 才是 /

那能不能直接使用 / 而不是 /studyjavaservlet/? 毕竟 / 比较简洁。

答案是肯定的。先关闭 Tomcat(执行 shutdown.shshutdown.bat),然后删除 Tomcat 的 webapps 目录下的所有文件夹和文件,最后把我们的 studyjavaservlet.war 复制过来,改名为 ROOT.war,文件名为 ROOT 的应用程序将作为默认应用,启动后直接访问 http://localhost:8080/ 即可。

容器的原理

实际上,类似 Tomcat 这样的服务器也是 Java 编写的,启动 Tomcat 服务器实际上是启动 Java 虚拟机,执行 Tomcat 的 main() 方法,然后由 Tomcat 负责加载 .war 文件,并创建一个 HelloServlet 实例,最后以多线程的模式来处理 HTTP 请求。

如果 Tomcat 服务器收到的请求路径是 /(假定部署文件为 ROOT.war),就转发到 HelloServlet 并传入 HttpServletRequest 和 HttpServletResponse 两个对象。

因为我们编写的 Servlet 并不是直接运行,而是由 Web 服务器加载后创建实例运行,所以,类似 Tomcat 这样的 Web 服务器也称为 Servlet 容器。

在 Servlet 容器中运行的 Servlet 具有如下特点:

  • 无法在代码中直接通过 new 创建 Servlet 实例,必须由 Servlet 容器自动创建Servlet 实例;
  • Servlet 容器只会给每个 Servlet 类创建唯一实例;
  • Servlet 容器会使用多线程执行 doGet()doPost() 方法。

所以复习一下 Java多线程的内容,我们可以得出结论:

  • 在 Servlet 中定义的实例变量会被多个线程同时访问,要注意线程安全;
  • HttpServletRequest 和 HttpServletResponse 实例是由 Servlet 容器传入的局部变量,它们只能被当前线程访问,不存在多个线程访问的问题;
  • doGet()doPost() 方法中,如果使用了 ThreadLocal,但没有清理,那么它的状态很可能会影响到下次的某个请求,因为 Servlet 容器很可能用线程池实现线程复用。

使用嵌入式 Tomcat 开发

上面那种手动丢到 Tomcat的 webapp 目录的方式实在太麻烦了,而且它调试还需要打开 Tomcat 的远程调试端口并且连接上去。

  • 启动 JVM并执行 Tomcat 的 main() 方法;
  • 加载 war 并初始化 Servlet;
  • 正常服务。

启动 Tomcat 无非就是设置好 classpath 并执行 Tomcat 某个 jar 包的 main() 方法,我们完全可以把 Tomcat 的 jar 包全部引入进来,然后自己编写一个 main() 方法,先启动 Tomcat,然后让它加载我们的 webapp 就行。

<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.itranswarp.learnjava</groupId>
<artifactId>web-servlet-embedded</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<java.version>11</java.version>
<tomcat.version>9.0.26</tomcat.version>
</properties>

<dependencies>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>${tomcat.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>${tomcat.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

引入依赖 tomcat-embed-core 和 tomcat-embed-jasper,引入的 Tomcat 版本 <tomcat.version> 为9.0.26。

不必引入 Servlet API,因为引入 Tomcat 依赖后自动引入了 Servlet API。因此,我们可以正常编写 Servlet 如下:

@WebServlet(urlPatterns = "/")
public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html");
String name = req.getParameter("name");
if (name == null) {
name = "world";
}
PrintWriter pw = resp.getWriter();
pw.write("<h1>Hello, " + name + "!</h1>");
pw.flush();
}
}

然后,我们编写一个 main() 方法,启动Tomcat服务器:

public class Main {
public static void main(String[] args) throws Exception {
// 启动 Tomcat:
Tomcat tomcat = new Tomcat();
tomcat.setPort(Integer.getInteger("port", 8080));
tomcat.getConnector();

// 创建 webapp:
Context ctx = tomcat.addWebapp("", new File("src/main/webapp").getAbsolutePath());
WebResourceRoot resources = new StandardRoot(ctx);
resources.addPreResources(
new DirResourceSet(resources, "/WEB-INF/classes", new File("target/classes").getAbsolutePath(), "/"));
ctx.setResources(resources);
tomcat.start();
tomcat.getServer().await();
}
}

这样,我们直接运行 main() 方法,即可启动嵌入式 Tomcat 服务器,然后,通过预设的 tomcat.addWebapp("", new File("src/main/webapp"),Tomcat 会自动加载当前工程作为根 webapp,可直接在浏览器访问 http://localhost:8080/

Reference

参考资料 廖雪峰的 Web基础 参考资料 菜鸟教程的 Java Servlet 教程 参考资料 Difference between servlet and web service